DOM (文档对象模型)结构是一个树型结构。当一个 HTML 元素产生一个事件时,该事件会在元素节点与根节点之间的路径传播,路径所经过的节点都会收到该事件,这个传播过程可称为 DOM 事件流。
捕获型事件( Capturing ): Netscape Navigator 的实现,与冒泡型刚好相反,由 DOM 树最顶层元素一直到最精确的元素。
冒泡型事件( Bubbling ):从 DOM 树型结构上理解,就是事件由叶子节点沿祖先节点一直向上传递直到根节点;从浏览器界面视图 HTML 元素排列层次上理解,就是事件由具有从属关系的最确定的目标元素一直传递到最不确定的目标元素。
DOM 标准事件模型由于两个不同的模型都有其优点和解释, DOM 标准支持捕获型与冒泡型,可以说是它们两者的结合体。它可以在一个 DOM 元素上绑定多个事件处理器,并且在处理函数内部, this 关键字仍然指向被绑定的 DOM 元素,另外,处理函数参数列表的第一个位置传递事件 Event 对象。
首先是捕获式传递事件,接着是冒泡式传递。所以,如果一个处理函数既注册了捕获型事件的监听,又注册了冒泡型事件监听,那么在 DOM 事件模型中它就会被调用两次。
W3C 对 DOM 事件定义了 3个阶段:捕获阶段、目标阶段和冒泡阶段。
每当事件被触发时,事件从 DOM 最外层的元素(在 DOM 树型结构最顶端的元素,这里就是 body 元素)开始,然后沿着最短的路径向 target (目标节点)遍历,这就是捕获阶段。
当到达目标节点后,事件就转到目标节点阶段。然后沿着 DOM 树上溯到最外层的节点,这个过程就是冒泡( bubbling )。
可以定义如下代码来检测(注意,这里又定义了一个 div 元素用于显示检测的输出结果):
<body>
<div id="showResult" style="width:700px;text-align:left;"></div>
<script>
// 获取 div 元素的引用
var oBody = document.body;
var oDivParent = document.getElementById('divParent');
var oDivChildA = document.getElementById('divChildA');
var oResult = document.getElementById('showResult');
// 检测事件所处阶段
function checkPhase(evt) {
var phase;
// 使用 eventPhase 属性可以获取事件所处阶段的信息
// // 低版本 IE 没有定义该属性
if (evt.eventPhase == 1) {
phase = '捕获阶段 ';
} else if (evt.eventPhase == 2) {
phase = '目标阶段 ';
} else if (evt.eventPhase == 3) {
phase = '冒泡阶段 ';
}
return phase;
}
var oBodyClick = function (evt) {
oResult.innerHTML +=
' === === == [body] === ' + checkPhase(evt) + ' === === == ';
oResult.innerHTML += '[evt.currentTarget] = ' + evt.currentTarget.id + '';
oResult.innerHTML += '[evt.target] = ' + evt.target.id + ' ';
};
var oDivParentClick = function (evt) {
oResult.innerHTML +=
' === === == [divParent] === ' + checkPhase(evt) + ' === === ==';
oResult.innerHTML +=
'[evt.currentTarget] = ' + evt.currentTarget.id + ' ';
oResult.innerHTML += '[evt.target] = ' + evt.target.id + ' ';
};
var oDivChildAClick = function (evt) {
oResult.innerHTML +=
' ====== == [divChildA] === ' + checkPhase(evt) + ' === === == ';
oResult.innerHTML +=
'[evt.currentTarget] = ' + evt.currentTarget.id + ' ';
oResult.innerHTML += '[evt.target] = ' + evt.target.id + ' ';
};
oDivChildA.addEventListener('click', oDivChildAClick, true);
oDivParent.addEventListener('click', oDivParentClick, true);
oBody.addEventListener('click', oBodyClick, true);
</script>
</body>
当鼠标在 divChildA 上触发单击事件时,首先捕捉到该事件的是根节点,也就是 body 元素,然后事件逐层向下传递,先到 divParent ,再到 divChildA ,这是捕获阶段。
当鼠标在 divChildB 上触发单击事件时,首先捕捉到该事件的也是根节点,也就是 body 元素,然后事件逐层向下传递,先到 divParent ,再到 divChildB 这也是捕获阶段。
注意事件的触发顺序,并且还要注意 currentTarget 和 target 属性的区别: target 属性的值始终是 divChildA ,而 currentTarget 属性则有所变化( body 元素因为没有定义 id 属性,所以是空值)。
从特定目标向不特定的目标( document 对象)一次触发事件,也就是说从下向上进行响应,这个过程被形象的称为 "冒泡 "。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width,
initial-scale=1.0"
/>
<title>冒个泡泡</title>
<style>
div {
margin: 20px;
border: solid 1px blue;
font-size: 20px;
}
</style>
<script>
function bubble() {
var div = document.getElementsByTagName('div');
var show = document.getElementById('show');
for (var i = 0; i < div.length; i++) {
div[i].onclick = (function (i) {
return function () {
div[i].style.border = '1px dashed #f00';
show.innerHTML += div[i].className + ' > ';
};
})(i);
}
}
window.onload = bubble;
</script>
</head>
<body>
<div class="div-1">
div-1
<div class="div-2">
div-2
<div class="div-3">
div-3
<div class="div-4">
div-4
<div class="div-5">div-5</div>
</div>
</div>
</div>
</div>
<p id="show"></p>
</body>
</html>
事件从不特定的目标( document 对象)开始触发,最后向特定的目标。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width,
initial-scale=1.0"
/>
<title>冒个泡泡</title>
<style>
div {
margin: 20px;
border: solid 1px blue;
font-size: 20px;
}
</style>
<script>
function bubble() {
var div = document.getElementsByTagName('div');
var show = document.getElementById('show');
for (var i = 0; i < div.length; i++) {
div[i].addEventListener(
'click',
(function (i) {
return function () {
div[i].style.border = '1px dashed #f00';
show.innerHTML += div[i].className + ' > ';
};
})(i),
true,
);
}
}
window.onload = bubble;
</script>
</head>
<body>
<div class="div-1">
div-1
<div class="div-2">
div-2
<div class="div-3">
div-3
<div class="div-4">
div-4
<div class="div-5">div-5</div>
</div>
</div>
</div>
</div>
<p id="show"></p>
</body>
</html>
W3C 的 DOM 支持捕获型和冒泡型两种事件流,但是捕获型事件流会先发生,然后才是冒泡型事件流。两种事件流会触及 DOM 中的所有层级对象,从 document 对象开始,最后返回 document 对象结束。其中,分为三个阶段:捕获阶段、目标阶段、冒泡阶段。
从前面的范例可以看到,对于 divChildB ,经过捕捉阶段进入目标阶段就停止了,当鼠标在 divChildA 上单击时,由于定义了事件监听函数,在目标阶段会触发事件,而 divChildB 没有定义监听函数,所以,当单击时不会有响应。
在处理了目标阶段的事件后就进入了冒泡阶段,之所以称之为冒泡,是因为它沿着原来的路径回溯。先到父节点 divParent ,直到根节点为止,这个过程就像是冒泡。
要触发冒泡阶段的事件,就必须设置 addEventListener()方法注册事件监听时参数 useCapture 为 false ,这也是低版本 IE 所支持的方式:
oDivChildA.addEventListener('click', oDivChildAClick, false);
oDivParent.addEventListener('click', oDivParentClick, false);
oBody.addEventListener('click', oBodyClick, false);
使用下面的代码可以检测旧版 IE 的工作情况:
<div id="showResult" style="width:700px;text-align:left;"></div>
<script>
//获取 div 元素的引用
var oBody = document.body;
var oDivParent = document.getElementById('divParent');
var oDivChildA = document.getElementById('divChildA');
var oResult = document.getElementById('showResult');
var oBodyClick = function () {
var evt = window.event;
oResult.innerHTML += ' === === [body] === === < br / > ';
oResult.innerHTML +=
'[evt.srcElement] = ' + evt.srcElement.id + ' < br / > ';
};
var oDivParentClick = function () {
var evt = window.event;
oResult.innerHTML += ' === === [divParent] === === <br /> ';
oResult.innerHTML += '[evt.srcElement] = ' + evt.srcElement.id + ' <br /> ';
};
var oDivChildAClick = function () {
var evt = window.event;
oResult.innerHTML += ' === === [divChildA] === === <br/> ';
oResult.innerHTML += '[evt.srcElement] = ' + evt.srcElement.id + ' <br/> ';
};
oDivChildA.attachEvent('onclick', oDivChildAClick);
oDivParent.attachEvent('onclick', oDivParentClick);
oBody.attachEvent('onclick', oBodyClick);
</script>
注意事件触发的顺序是一个冒泡过程。也可以使用 cancelBubble 属性取消冒泡阶段,该属性要在注册事件监听程序后定义:
oDivChildA.attachEvent('onclick', oDivChildAClick);
window.event.cancelBubble = true;
oDivParent.attachEvent('onclick', oDivParentClick);
oBody.attachEvent('onclick', oBodyClick);
// W3C DOM 使用 stopPropagation()方法实现取消冒泡阶段:
window.event.stopPropagation();
取消浏览器的事件传递。
取消事件传递是指停止捕获型事件或冒泡型事件的进一步传递。例如,上面的冒泡型事件传递中,在 body 处理停止事件传递后,位于上层的 document 的事件监听器就不再收到通知,不再被处理。
在 IE 下,通过设置 Event 对象的 cancelBubble 为 true 即可。
function someHandle() {
window.event.cancelBubble = true;
}
在 DOM 标准下,通过调用 Event 对象的 stopPropagation 方法即可。
function someHandle(event) {
event.stopPropagation();
}
因此,跨浏览器的取消事件传递的方法是:
function someHandle(event) {
event = event || window.event;
if (event.stopPropagation) event.stopPropagation();
else event.cancelBubble = true;
}
事件传递后的默认处理是指,通常浏览器在事件传递并处理完后会执行与该事件关联的默认动作(如果存在这样的动作)。
在 IE 下,通过设置 Event 对象的 returnValue 为 false 即可。
function someHandle() {
window.event.returnValue = false;
}
在 DOM 标准下,通过调用 Event 对象的 preventDefault 方法即可。
function someHandle(event) {
event.preventDefault();
}
因此,跨浏览器的取消事件传递后的默认处理的方法是:
function someHandle(event) {
event = event || window.event;
if (event.preventDefault) event.preventDefault();
else event.returnValue = false;
}